package com.gutsyzhan.bilibili.util;

import com.github.tobato.fastdfs.domain.fdfs.FileInfo;
import com.github.tobato.fastdfs.domain.fdfs.MetaData;
import com.github.tobato.fastdfs.domain.fdfs.StorePath;
import com.github.tobato.fastdfs.service.AppendFileStorageClient;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import com.gutsyzhan.bilibili.constant.FastDFSConstant;
import com.gutsyzhan.bilibili.exception.ConditionException;
import com.mysql.jdbc.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.*;

@Component
public class FastDFSUtil {
    @Resource
    private FastFileStorageClient fastFileStorageClient;

    @Resource
    private AppendFileStorageClient appendFileStorageClient;

    @Resource
    private RedisTemplate<String, String> redisTemplate;

    @Value("${fdfs.http.storage-addr}")
    private String fdfsHttpStorageAddr;

    /**
     * 获取文件类型
     */
    public String getFileType(MultipartFile file){
        if(file == null){
           throw new ConditionException("非法文件!");
        }
        String fileName = file.getOriginalFilename();
        int index = fileName.lastIndexOf(".");
        return fileName.substring(index+1);
    }

    /**
     * 上传普通文件
     */
    public String uploadCommonFile(MultipartFile file) throws Exception {
        Set<MetaData> metaDataSet = new HashSet<>();
        String fileType = getFileType(file);
        StorePath storePath = fastFileStorageClient.uploadFile(file.getInputStream(), file.getSize(), fileType, metaDataSet);
        return storePath.getPath();
    }

    /**
     * 上传大文件（支持断点续传）
     */
    public String uploadAppenderFile(MultipartFile file) throws Exception {
        String fileType = getFileType(file);
        StorePath storePath = appendFileStorageClient.uploadAppenderFile(FastDFSConstant.DEFAULT_GROUP, file.getInputStream(), file.getSize(), fileType);
        return storePath.getPath();
    }

    /**
     * 修改续传文件的内容
     * @param file 文件
     * @param filePath  文件路径
     * @param offset  已上传的文件大小
     * @throws Exception
     */
    public void modifyAppenderFile(MultipartFile file, String filePath, long offset) throws Exception {
        appendFileStorageClient.modifyFile(FastDFSConstant.DEFAULT_GROUP, filePath, file.getInputStream(), file.getSize(), offset);
    }


    /**
     * 通过分片来上传文件
     * @param file 文件
     * @param fileMd5  文件对应的md5值
     * @param sliceNo  当前需要上传的分片是第几片，从1开始
     * @param totalSliceNo  总共要上传的分片数，用于判断何时文件上传完成
     * @return 上传后的文件路径
     */
    public String uploadFileBySlices(MultipartFile file, String fileMd5, Integer sliceNo, Integer totalSliceNo) throws Exception {
        //1、参数校验
        if(file == null || StringUtils.isNullOrEmpty(fileMd5) || sliceNo == null || totalSliceNo == null){
            throw new ConditionException("参数异常");
        }
        //第一个分片上传后返回的路径
        String pathKey = FastDFSConstant.PATH_KEY + fileMd5;
        //当前已上传的所有分片文件大小
        String uploadedSizeKey = FastDFSConstant.UPLOADED_SIZE_KEY + fileMd5;
        //当前已上传的分片个数，注意是与分片总数进行比对，当两者一样，说明已完成所有分片的上传，可以结束分片上传流程
        String uploadedNoKey = FastDFSConstant.UPLOADED_NO_KEY + fileMd5;

        //2、判断当前是否已上传过文件，需要考虑当前是第一个分片还是除此之外的其他分片
        String uploadedSizeStr = redisTemplate.opsForValue().get(uploadedSizeKey);
        Long uploadedSize = 0L;
        if(!StringUtils.isNullOrEmpty(uploadedSizeStr)){
            uploadedSize = Long.valueOf(uploadedSizeStr);
        }
        //3、判断当前是否为第一个分片
        if(sliceNo == 1){
            //第一个分片则调用uploadAppenderFile方法，获取文件的上传路径
            String path = uploadAppenderFile(file);
            if(StringUtils.isNullOrEmpty(path)){
                throw new ConditionException("上传失败!");
            }
            //将已上传的文件路径保存在Redis
            redisTemplate.opsForValue().set(pathKey, path);
            //更新Redis中已上传的分片个数
            redisTemplate.opsForValue().set(uploadedNoKey, "1");
        }else{
            //获取第一个分片上传的路径
            String filePath = redisTemplate.opsForValue().get(pathKey);
            if(StringUtils.isNullOrEmpty(filePath)){
                throw new ConditionException("上传失败!");
            }
            //不是第一个分片则调用modifyAppenderFile方法，修改已上传的分片信息
            modifyAppenderFile(file, filePath, uploadedSize);
            //更新Redis中已上传的分片个数
            //由于我们进行了序列化，因此这里不能使用redisTemplate.opsForValue().increment(uploadedNoKey);，因为此时它是字符串
            Integer beforeUploadedNoValue = Integer.parseInt(redisTemplate.opsForValue().get(uploadedNoKey));
            Integer curUploadedNoValue = beforeUploadedNoValue +1;
            redisTemplate.opsForValue().set(uploadedNoKey,curUploadedNoValue+"");
        }
        //4、更新Redis中已上传的所有分片文件大小
        uploadedSize += file.getSize();
        redisTemplate.opsForValue().set(uploadedSizeKey, String.valueOf(uploadedSize));
        //5、判断文件上传是否上传完毕
        String uploadedNoStr = redisTemplate.opsForValue().get(uploadedNoKey);
        Integer uploadedNo = Integer.valueOf(uploadedNoStr);
        //保留上传路径
        String resultPath = "";
        if(Objects.equals(uploadedNo, totalSliceNo)){
            //说明文件已经上传完成，需要清空Redis中相关的key和value信息
            resultPath = redisTemplate.opsForValue().get(pathKey);
            List<String> keyList = Arrays.asList(pathKey, uploadedSizeKey, uploadedNoKey);
            redisTemplate.delete(keyList);
        }
        return resultPath;
    }

    /**
     * 对文件进行分片处理
     * @param multipartFile
     */
    public void convertFileToSlices(MultipartFile multipartFile) throws Exception {
        String fileType = getFileType(multipartFile);
        //将multipartFile转换为file这一临时文件
        File file = multipartFileToFile(multipartFile);
        //获取文件长度
        long fileLength = file.length();
        //定义一个计数器，用于给分片编号
        int count = 1;
        for (int i = 0; i < fileLength; i += FastDFSConstant.SLICE_SIZE) {
            //定义到需要分配的文件起始点
            //这里我们需要使用到randomAccessFile，因为它可以根据索引搜寻到我们需要的指定位置，r表示只读
            RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
            randomAccessFile.seek(i);
            //通过上面两行代码，我们已经定位到需要做分片的起始位置
            //那么接下来就只需从这个起始位置读取一定数量(SLICE_SIZE)的数据即可
            //由于每次读取SLICE_SIZE数量，因此构造一个读取的字节数组
            byte[] bytes = new byte[FastDFSConstant.SLICE_SIZE];
            //获取每次实际读取的字节数量，因为最后一个分片的长度可能会比SLICE_SIZE的长度要小
            int len = randomAccessFile.read(bytes);
            //定义分片的存储路径(路径类似于E:/TempFiles/MyBilibili/1.mp4)
            String slicePath = FastDFSConstant.SLICE_PATH + count + "."+ fileType;
            //定义分片文件对象
            File sliceFile = new File(slicePath);
            //构建输出流
            FileOutputStream fos = new FileOutputStream(sliceFile);
            //将输出流写入到分片文件对象中
            fos.write(bytes, 0, len);
            //关闭流对象
            fos.close();
            randomAccessFile.close();
            //分片编号记得加1
            count ++;
        }
        //删除生成的临时文件
        file.delete();

    }

    public File multipartFileToFile(MultipartFile multipartFile) throws Exception {
        File file = null;
        try{
            String originalFilename = multipartFile.getOriginalFilename();
            //获取文件后缀
            String prefix = originalFilename.substring(originalFilename.lastIndexOf("."));
            //创建临时文件
            file = File.createTempFile(originalFilename, prefix);
            //调用transferTo方法，将MultipartFile转为目标File文件
            multipartFile.transferTo(file);
            //删除原文件
            file.deleteOnExit();
        }catch (IOException e){
            e.printStackTrace();
        }
        return file;
    }


    /**
     * 删除文件
     */
    public void deleteFile(String filePath){
        fastFileStorageClient.deleteFile(filePath);
    }

    /**
     * 在线播放视频
     */
    public void viewVideoOnlineBySlices(HttpServletRequest request, HttpServletResponse response, String path) throws Exception {
        FileInfo fileInfo = fastFileStorageClient.queryFileInfo(FastDFSConstant.DEFAULT_GROUP, path);
        long totalFileSize = fileInfo.getFileSize();
        //构建完整的服务器视频地址
        String url = fdfsHttpStorageAddr + path;
        //获取前端传递的请求头信息
        Enumeration<String> headerNames = request.getHeaderNames();
        Map<String ,Object> headers = new HashMap<>();
        while (headerNames.hasMoreElements()){
            String header = headerNames.nextElement();
            headers.put(header, request.getHeader(header));
        }
        //获取请求头中的Range，这是一个Map对象，举个例子，它的值为bytes=12421-41515
        String rangeStr = request.getHeader("Range");
        String[] range;
        if(StringUtils.isNullOrEmpty(rangeStr)){
            //赋初始值
            rangeStr = "bytes=0-" + (totalFileSize -1);
        }
        //将rangeStr按照bytes=或者-号进行分割，分割成三部分，第一步是空的，第二和三部分才有值
        range = rangeStr.split("bytes=|-");
        long begin = 0;
        if(range.length >= 2){
            begin = Long.parseLong(range[1]);
        }
        long end = totalFileSize -1;
        if(range.length >= 3){
            end = Long.parseLong(range[2]);
        }
        long len = (end - begin) +1;
        //构建视频分片内容范围Content-Range这一键值对，举个例子，值可能为bytes 351551-642622/642623
        //注意前面的是当前视频分片的起始和终止位置，而后面的则是视频总的大小
        String contentRange = "bytes "+ begin +"-" + end + "/" + totalFileSize;
        response.setHeader("Content-Range", contentRange);
        //构造其他的请求头
        response.setHeader("Accept-Ranges", "bytes");
        response.setHeader("Content-Type", "video/mp4");
        response.setContentLength((int)len);
        response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
        HttpUtil.get(url ,headers, response);
    }
}
